/* Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package it.localhost.trafficdroid.common.billing;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONException;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;

import com.android.vending.billing.IInAppBillingService;

/**
 * Provides convenience methods for in-app billing. Create one instance of this
 * class for your application and use it to process in-app billing operations.
 * It provides synchronous (blocking) and asynchronous (non-blocking) methods
 * for many common in-app billing operations, as well as automatic signature
 * verification.
 * 
 * After instantiating, you must perform setup in order to start using the
 * object. To perform setup, call the {@link #startSetup} method and provide a
 * listener; that listener will be notified when setup is complete, after which
 * (and not before) you may call other methods.
 * 
 * After setup is complete, you may query whether the user owns a given item or
 * not by calling {@link #isOwned}, get all items owned with
 * {@link #getOwnedSkus}, get an item's price with {@link #getPrice}, amongst
 * others (see documentation for specific methods).
 * 
 * Please notice that the object will only have knowledge about owned items; it
 * will not automatically have information (such as price, description) for
 * items that are not owned by the user, because the server will not
 * automatically provide those. In order to query information for an item that's
 * not owned (such as to display the price to the user before a purchase), you
 * should first bring the item's sku to the object's knowledge by calling
 * {@link #addSku} and then perform an inventory refresh by calling
 * {@link #refreshInventory()} or its corresponding asynchronous version
 * {@link #refreshInventoryAsync}.
 * 
 * If you know the skus of all the items that you can possibly be interested in,
 * you can call {@link #addSku} for those items before {@link #startSetup}, and
 * that way all the information about them will be available from the start,
 * with no need to refresh the inventory later.
 * 
 * When you are done with this object, don't forget to call {@link #dispose} to
 * ensure proper cleanup. This object holds a binding to the in-app billing
 * service, which will leak unless you dispose of it correctly. If you created
 * the object on an Activity's onCreate method, then the recommended place to
 * dispose of it is the Activity's onDestroy method.
 * 
 * A note about threading: When using this object from a background thread, you
 * may call the blocking versions of methods; when using from a UI thread, call
 * only the asynchronous versions and handle the results via callbacks. Also,
 * notice that you can only call one asynchronous operation at a time;
 * attempting to start a second asynchronous operation while the first one has
 * not yet completed will result in an exception being thrown.
 * 
 * @author Bruno Oliveira (Google)
 * 
 */
public class IabHelper {
	// Is debug logging enabled?
	boolean mDebugLog = false;
	String mDebugTag = "IabHelper";
	// Is setup done?
	boolean mSetupDone = false;
	// Is an asynchronous operation in progress?
	// (only one at a time can be in progress)
	boolean mAsyncInProgress = false;
	// (for logging/debugging)
	// if mAsyncInProgress == true, what asynchronous operation is in progress?
	String mAsyncOperation = "";
	// Context we were passed during initialization
	Context mContext;
	// Connection to the service
	IInAppBillingService mService;
	ServiceConnection mServiceConn;
	// The request code used to launch purchase flow
	int mRequestCode;
	// Public key for verifying signature, in base64 encoding
	String mSignatureBase64 = null;
	// Billing response codes
	public static final int BILLING_RESPONSE_RESULT_OK = 0;
	public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
	public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
	public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
	public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
	public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
	public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
	public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
	// IAB Helper error codes
	public static final int IABHELPER_ERROR_BASE = -1000;
	public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
	public static final int IABHELPER_BAD_RESPONSE = -1002;
	public static final int IABHELPER_VERIFICATION_FAILED = -1003;
	public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
	public static final int IABHELPER_USER_CANCELLED = -1005;
	public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
	public static final int IABHELPER_MISSING_TOKEN = -1007;
	public static final int IABHELPER_UNKNOWN_ERROR = -1008;
	// Keys for the responses from InAppBillingService
	public static final String RESPONSE_CODE = "RESPONSE_CODE";
	public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
	public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
	public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
	public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
	public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
	public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
	public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
	public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
	// Item type: in-app item
	public static final String ITEM_TYPE_INAPP = "inapp";
	// some fields on the getSkuDetails response bundle
	public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
	public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";

	/**
	 * Creates an instance. After creation, it will not yet be ready to use. You
	 * must perform setup by calling {@link #startSetup} and wait for setup to
	 * complete. This constructor does not block and is safe to call from a UI
	 * thread.
	 * 
	 * @param ctx
	 *            Your application or Activity context. Needed to bind to the
	 *            in-app billing service.
	 * @param base64PublicKey
	 *            Your application's public key, encoded in base64. This is used
	 *            for verification of purchase signatures. You can find your
	 *            app's base64-encoded public key in your application's page on
	 *            Google Play Developer Console. Note that this is NOT your
	 *            "developer public key".
	 */
	public IabHelper(Context ctx, String base64PublicKey) {
		mContext = ctx.getApplicationContext();
		mSignatureBase64 = base64PublicKey;
		logDebug("IAB helper created.");
	}

	/**
	 * Enables or disable debug logging through LogCat.
	 */
	public void enableDebugLogging(boolean enable, String tag) {
		mDebugLog = enable;
		mDebugTag = tag;
	}

	public void enableDebugLogging(boolean enable) {
		mDebugLog = enable;
	}

	/**
	 * Callback for setup process. This listener's {@link #onIabSetupFinished}
	 * method is called when the setup process is complete.
	 */
	public interface OnIabSetupFinishedListener {
		/**
		 * Called to notify that setup is complete.
		 * 
		 * @param result
		 *            The result of the setup process.
		 */
		public void onIabSetupFinished(IabResult result);
	}

	/**
	 * Starts the setup process. This will start up the setup process
	 * asynchronously. You will be notified through the listener when the setup
	 * process is complete. This method is safe to call from a UI thread.
	 * 
	 * @param listener
	 *            The listener to notify when the setup process is complete.
	 */
	public void startSetup(final OnIabSetupFinishedListener listener) {
		// If already set up, can't do it again.
		if (mSetupDone)
			throw new IllegalStateException("IAB helper is already set up.");
		// Connection to IAB service
		logDebug("Starting in-app billing setup.");
		mServiceConn = new ServiceConnection() {
			@Override
			public void onServiceDisconnected(ComponentName name) {
				logDebug("Billing service disconnected.");
				mService = null;
			}

			@Override
			public void onServiceConnected(ComponentName name, IBinder service) {
				logDebug("Billing service connected.");
				mService = IInAppBillingService.Stub.asInterface(service);
				String packageName = mContext.getPackageName();
				try {
					logDebug("Checking for in-app billing 3 support.");
					int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
					if (response != BILLING_RESPONSE_RESULT_OK) {
						if (listener != null)
							listener.onIabSetupFinished(new IabResult(response, "Error checking for billing v3 support."));
						return;
					}
					logDebug("In-app billing version 3 supported for " + packageName);
					mSetupDone = true;
				} catch (RemoteException e) {
					if (listener != null) {
						listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, "RemoteException while setting up in-app billing."));
					}
					e.printStackTrace();
				}
				if (listener != null) {
					listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
				}
			}
		};
		mContext.bindService(new Intent("com.android.vending.billing.InAppBillingService.BIND"), mServiceConn, Context.BIND_AUTO_CREATE);
	}

	/**
	 * Dispose of object, releasing resources. It's very important to call this
	 * method when you are done with this object. It will release any resources
	 * used by it such as service connections. Naturally, once the object is
	 * disposed of, it can't be used again.
	 */
	public void dispose() {
		logDebug("Disposing.");
		mSetupDone = false;
		if (mServiceConn != null) {
			logDebug("Unbinding from service.");
			if (mContext != null)
				mContext.unbindService(mServiceConn);
			mServiceConn = null;
			mService = null;
			mPurchaseListener = null;
		}
	}

	/**
	 * Callback that notifies when a purchase is finished.
	 */
	public interface OnIabPurchaseFinishedListener {
		/**
		 * Called to notify that an in-app purchase finished. If the purchase
		 * was successful, then the sku parameter specifies which item was
		 * purchased. If the purchase failed, the sku and extraData parameters
		 * may or may not be null, depending on how far the purchase process
		 * went.
		 * 
		 * @param result
		 *            The result of the purchase.
		 * @param info
		 *            The purchase information (null if purchase failed)
		 */
		public void onIabPurchaseFinished(IabResult result, Purchase info);
	}

	// The listener registered on launchPurchaseFlow, which we have to call back when
	// the purchase finishes
	OnIabPurchaseFinishedListener mPurchaseListener;

	/**
	 * Same as calling
	 * {@link #launchPurchaseFlow(Activity, String, int, OnIabPurchaseFinishedListener, String)}
	 * with null as extraData.
	 */
	public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
		launchPurchaseFlow(act, sku, requestCode, listener, "");
	}

	/**
	 * Initiate the UI flow for an in-app purchase. Call this method to initiate
	 * an in-app purchase, which will involve bringing up the Google Play
	 * screen. The calling activity will be paused while the user interacts with
	 * Google Play, and the result will be delivered via the activity's
	 * {@link android.app.Activity#onActivityResult} method, at which point you
	 * must call this object's {@link #handleActivityResult} method to continue
	 * the purchase flow. This method MUST be called from the UI thread of the
	 * Activity.
	 * 
	 * @param act
	 *            The calling activity.
	 * @param sku
	 *            The sku of the item to purchase.
	 * @param requestCode
	 *            A request code (to differentiate from other responses -- as in
	 *            {@link android.app.Activity#startActivityForResult}).
	 * @param listener
	 *            The listener to notify when the purchase process finishes
	 * @param extraData
	 *            Extra data (developer payload), which will be returned with
	 *            the purchase data when the purchase completes. This extra data
	 *            will be permanently bound to that purchase and will always be
	 *            returned when the purchase is queried.
	 */
	public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener, String extraData) {
		checkSetupDone("launchPurchaseFlow");
		flagStartAsync("launchPurchaseFlow");
		IabResult result;
		try {
			logDebug("Constructing buy intent for " + sku);
			Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, ITEM_TYPE_INAPP, extraData);
			int response = getResponseCodeFromBundle(buyIntentBundle);
			if (response != BILLING_RESPONSE_RESULT_OK) {
				logError("Unable to buy item, Error response: " + getResponseDesc(response));
				result = new IabResult(response, "Unable to buy item");
				if (listener != null)
					listener.onIabPurchaseFinished(result, null);
			}
			PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
			logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
			mRequestCode = requestCode;
			mPurchaseListener = listener;
			act.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
		} catch (SendIntentException e) {
			logError("SendIntentException while launching purchase flow for sku " + sku);
			e.printStackTrace();
			result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
			if (listener != null)
				listener.onIabPurchaseFinished(result, null);
		} catch (RemoteException e) {
			logError("RemoteException while launching purchase flow for sku " + sku);
			e.printStackTrace();
			result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
			if (listener != null)
				listener.onIabPurchaseFinished(result, null);
		}
	}

/**
	 * Handles an activity result that's part of the purchase flow in in-app
	 * billing. If you are calling {@link #launchPurchaseFlow}, then you must
	 * call this method from your Activity's {@link android.app.Activity
	 * @onActivityResult} method. This method MUST be called from the UI thread
	 * of the Activity.
	 * 
	 * @param requestCode
	 *            The requestCode as you received it.
	 * @param resultCode
	 *            The resultCode as you received it.
	 * @param data
	 *            The data (Intent) as you received it.
	 * @return Returns true if the result was related to a purchase flow and was
	 *         handled; false if the result was not related to a purchase, in
	 *         which case you should handle it normally.
	 */
	public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
		IabResult result;
		if (requestCode != mRequestCode)
			return false;
		checkSetupDone("handleActivityResult");
		// end of async purchase operation
		flagEndAsync();
		if (data == null) {
			logError("Null data in IAB activity result.");
			result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
			if (mPurchaseListener != null)
				mPurchaseListener.onIabPurchaseFinished(result, null);
			return true;
		}
		int responseCode = getResponseCodeFromIntent(data);
		String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
		String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
		if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
			logDebug("Successful resultcode from purchase activity.");
			logDebug("Purchase data: " + purchaseData);
			logDebug("Data signature: " + dataSignature);
			logDebug("Extras: " + data.getExtras());
			if (purchaseData == null || dataSignature == null) {
				logError("BUG: either purchaseData or dataSignature is null.");
				logDebug("Extras: " + data.getExtras().toString());
				result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
				if (mPurchaseListener != null)
					mPurchaseListener.onIabPurchaseFinished(result, null);
				return true;
			}
			Purchase purchase = null;
			try {
				purchase = new Purchase(purchaseData, dataSignature);
				String sku = purchase.getSku();
				// Verify signature
				if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
					logError("Purchase signature verification FAILED for sku " + sku);
					result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
					if (mPurchaseListener != null)
						mPurchaseListener.onIabPurchaseFinished(result, purchase);
					return true;
				}
				logDebug("Purchase signature successfully verified.");
			} catch (JSONException e) {
				logError("Failed to parse purchase data.");
				e.printStackTrace();
				result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
				if (mPurchaseListener != null)
					mPurchaseListener.onIabPurchaseFinished(result, null);
				return true;
			}
			if (mPurchaseListener != null) {
				mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
			}
		} else if (resultCode == Activity.RESULT_OK) {
			// result code was OK, but in-app billing response was not OK.
			logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
			if (mPurchaseListener != null) {
				result = new IabResult(responseCode, "Problem purchashing item.");
				mPurchaseListener.onIabPurchaseFinished(result, null);
			}
		} else if (resultCode == Activity.RESULT_CANCELED) {
			logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
			result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
			if (mPurchaseListener != null)
				mPurchaseListener.onIabPurchaseFinished(result, null);
		} else {
			logError("Purchase failed. Result code: " + Integer.toString(resultCode) + ". Response: " + getResponseDesc(responseCode));
			result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
			if (mPurchaseListener != null)
				mPurchaseListener.onIabPurchaseFinished(result, null);
		}
		return true;
	}

	/**
	 * Queries the inventory. This will query all owned items from the server,
	 * as well as information on additional skus, if specified. This method may
	 * block or take long to execute. Do not call from a UI thread. For that,
	 * use the non-blocking version {@link #refreshInventoryAsync}.
	 * 
	 * @param querySkuDetails
	 *            if true, SKU details (price, description, etc) will be queried
	 *            as well as purchase information.
	 * @param moreSkus
	 *            additional skus to query information on, regardless of
	 *            ownership. Ignored if null or if querySkuDetails is false.
	 * @throws IabException
	 *             if a problem occurs while refreshing the inventory.
	 */
	public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
		checkSetupDone("queryInventory");
		try {
			Inventory inv = new Inventory();
			int r = queryPurchases(inv);
			if (r != BILLING_RESPONSE_RESULT_OK) {
				throw new IabException(r, "Error refreshing inventory (querying owned items).");
			}
			if (querySkuDetails) {
				r = querySkuDetails(inv, moreSkus);
				if (r != BILLING_RESPONSE_RESULT_OK) {
					throw new IabException(r, "Error refreshing inventory (querying prices of items).");
				}
			}
			return inv;
		} catch (RemoteException e) {
			throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
		} catch (JSONException e) {
			throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
		}
	}

	/**
	 * Listener that notifies when an inventory query operation completes.
	 */
	public interface QueryInventoryFinishedListener {
		/**
		 * Called to notify that an inventory query operation completed.
		 * 
		 * @param result
		 *            The result of the operation.
		 * @param inv
		 *            The inventory.
		 */
		public void onQueryInventoryFinished(IabResult result, Inventory inv);
	}

	/**
	 * Asynchronous wrapper for inventory query. This will perform an inventory
	 * query as described in {@link #queryInventory}, but will do so
	 * asynchronously and call back the specified listener upon completion. This
	 * method is safe to call from a UI thread.
	 * 
	 * @param querySkuDetails
	 *            as in {@link #queryInventory}
	 * @param moreSkus
	 *            as in {@link #queryInventory}
	 * @param listener
	 *            The listener to notify when the refresh operation completes.
	 */
	public void queryInventoryAsync(final boolean querySkuDetails, final List<String> moreSkus, final QueryInventoryFinishedListener listener) {
		final Handler handler = new Handler();
		checkSetupDone("queryInventory");
		flagStartAsync("refresh inventory");
		(new Thread(new Runnable() {
			public void run() {
				IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
				Inventory inv = null;
				try {
					inv = queryInventory(querySkuDetails, moreSkus);
				} catch (IabException ex) {
					result = ex.getResult();
				}
				flagEndAsync();
				final IabResult result_f = result;
				final Inventory inv_f = inv;
				handler.post(new Runnable() {
					public void run() {
						listener.onQueryInventoryFinished(result_f, inv_f);
					}
				});
			}
		})).start();
	}

	public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
		queryInventoryAsync(true, null, listener);
	}

	public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
		queryInventoryAsync(querySkuDetails, null, listener);
	}

	/**
	 * Consumes a given in-app product. Consuming can only be done on an item
	 * that's owned, and as a result of consumption, the user will no longer own
	 * it. This method may block or take long to return. Do not call from the UI
	 * thread. For that, see {@link #consumeAsync}.
	 * 
	 * @param itemInfo
	 *            The PurchaseInfo that represents the item to consume.
	 * @throws IabException
	 *             if there is a problem during consumption.
	 */
	void consume(Purchase itemInfo) throws IabException {
		checkSetupDone("consume");
		try {
			String token = itemInfo.getToken();
			String sku = itemInfo.getSku();
			if (token == null || token.equals("")) {
				logError("Can't consume " + sku + ". No token.");
				throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: " + sku + " " + itemInfo);
			}
			logDebug("Consuming sku: " + sku + ", token: " + token);
			int response = mService.consumePurchase(3, mContext.getPackageName(), token);
			if (response == BILLING_RESPONSE_RESULT_OK) {
				logDebug("Successfully consumed sku: " + sku);
			} else {
				logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
				throw new IabException(response, "Error consuming sku " + sku);
			}
		} catch (RemoteException e) {
			throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
		}
	}

	/**
	 * Callback that notifies when a consumption operation finishes.
	 */
	public interface OnConsumeFinishedListener {
		/**
		 * Called to notify that a consumption has finished.
		 * 
		 * @param purchase
		 *            The purchase that was (or was to be) consumed.
		 * @param result
		 *            The result of the consumption operation.
		 */
		public void onConsumeFinished(Purchase purchase, IabResult result);
	}

	/**
	 * Callback that notifies when a multi-item consumption operation finishes.
	 */
	public interface OnConsumeMultiFinishedListener {
		/**
		 * Called to notify that a consumption of multiple items has finished.
		 * 
		 * @param purchases
		 *            The purchases that were (or were to be) consumed.
		 * @param results
		 *            The results of each consumption operation, corresponding
		 *            to each sku.
		 */
		public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
	}

	/**
	 * Asynchronous wrapper to item consumption. Works like {@link #consume},
	 * but performs the consumption in the background and notifies completion
	 * through the provided listener. This method is safe to call from a UI
	 * thread.
	 * 
	 * @param purchase
	 *            The purchase to be consumed.
	 * @param listener
	 *            The listener to notify when the consumption operation
	 *            finishes.
	 */
	public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
		checkSetupDone("consume");
		List<Purchase> purchases = new ArrayList<Purchase>();
		purchases.add(purchase);
		consumeAsyncInternal(purchases, listener, null);
	}

	/**
	 * Same as {@link consumeAsync}, but for multiple items at once.
	 * 
	 * @param purchases
	 *            The list of PurchaseInfo objects representing the purchases to
	 *            consume.
	 * @param listener
	 *            The listener to notify when the consumption operation
	 *            finishes.
	 */
	public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
		checkSetupDone("consume");
		consumeAsyncInternal(purchases, null, listener);
	}

	/**
	 * Returns a human-readable description for the given response code.
	 * 
	 * @param code
	 *            The response code
	 * @return A human-readable string explaining the result code. It also
	 *         includes the result code numerically.
	 */
	public static String getResponseDesc(int code) {
		String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + "3:Billing Unavailable/4:Item unavailable/" + "5:Developer Error/6:Error/7:Item Already Owned/" + "8:Item not owned").split("/");
		String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + "-1002:Bad response received/" + "-1003:Purchase signature verification failed/" + "-1004:Send intent failed/" + "-1005:User cancelled/" + "-1006:Unknown purchase response/" + "-1007:Missing token/" + "-1008:Unknown error").split("/");
		if (code <= IABHELPER_ERROR_BASE) {
			int index = IABHELPER_ERROR_BASE - code;
			if (index >= 0 && index < iabhelper_msgs.length)
				return iabhelper_msgs[index];
			else
				return String.valueOf(code) + ":Unknown IAB Helper Error";
		} else if (code < 0 || code >= iab_msgs.length)
			return String.valueOf(code) + ":Unknown";
		else
			return iab_msgs[code];
	}

	// Checks that setup was done; if not, throws an exception.
	void checkSetupDone(String operation) {
		if (!mSetupDone) {
			logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
			throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
		}
	}

	// Workaround to bug where sometimes response codes come as Long instead of Integer
	int getResponseCodeFromBundle(Bundle b) {
		Object o = b.get(RESPONSE_CODE);
		if (o == null) {
			logDebug("Bundle with null response code, assuming OK (known issue)");
			return BILLING_RESPONSE_RESULT_OK;
		} else if (o instanceof Integer)
			return ((Integer) o).intValue();
		else if (o instanceof Long)
			return (int) ((Long) o).longValue();
		else {
			logError("Unexpected type for bundle response code.");
			logError(o.getClass().getName());
			throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
		}
	}

	// Workaround to bug where sometimes response codes come as Long instead of Integer
	int getResponseCodeFromIntent(Intent i) {
		Object o = i.getExtras().get(RESPONSE_CODE);
		if (o == null) {
			logError("Intent with no response code, assuming OK (known issue)");
			return BILLING_RESPONSE_RESULT_OK;
		} else if (o instanceof Integer)
			return ((Integer) o).intValue();
		else if (o instanceof Long)
			return (int) ((Long) o).longValue();
		else {
			logError("Unexpected type for intent response code.");
			logError(o.getClass().getName());
			throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
		}
	}

	void flagStartAsync(String operation) {
		if (mAsyncInProgress)
			throw new IllegalStateException("Can't start async operation (" + operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
		mAsyncOperation = operation;
		mAsyncInProgress = true;
		logDebug("Starting async operation: " + operation);
	}

	void flagEndAsync() {
		logDebug("Ending async operation: " + mAsyncOperation);
		mAsyncOperation = "";
		mAsyncInProgress = false;
	}

	int queryPurchases(Inventory inv) throws JSONException, RemoteException {
		// Query purchases
		logDebug("Querying owned items...");
		logDebug("Package name: " + mContext.getPackageName());
		boolean verificationFailed = false;
		String continueToken = null;
		do {
			logDebug("Calling getPurchases with continuation token: " + continueToken);
			Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), ITEM_TYPE_INAPP, continueToken);
			int response = getResponseCodeFromBundle(ownedItems);
			logDebug("Owned items response: " + String.valueOf(response));
			if (response != BILLING_RESPONSE_RESULT_OK) {
				logDebug("getPurchases() failed: " + getResponseDesc(response));
				return response;
			}
			if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
				logError("Bundle returned from getPurchases() doesn't contain required fields.");
				return IABHELPER_BAD_RESPONSE;
			}
			ArrayList<String> ownedSkus = ownedItems.getStringArrayList(RESPONSE_INAPP_ITEM_LIST);
			ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST);
			ArrayList<String> signatureList = ownedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST);
			for (int i = 0; i < purchaseDataList.size(); ++i) {
				String purchaseData = purchaseDataList.get(i);
				String signature = signatureList.get(i);
				String sku = ownedSkus.get(i);
				if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
					logDebug("Sku is owned: " + sku);
					Purchase purchase = new Purchase(purchaseData, signature);
					if (TextUtils.isEmpty(purchase.getToken())) {
						logWarn("BUG: empty/null token!");
						logDebug("Purchase data: " + purchaseData);
					}
					// Record ownership and token
					inv.addPurchase(purchase);
				} else {
					logWarn("Purchase signature verification **FAILED**. Not adding item.");
					logDebug("   Purchase data: " + purchaseData);
					logDebug("   Signature: " + signature);
					verificationFailed = true;
				}
			}
			continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
			logDebug("Continuation token: " + continueToken);
		} while (!TextUtils.isEmpty(continueToken));
		return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
	}

	int querySkuDetails(Inventory inv, List<String> moreSkus) throws RemoteException, JSONException {
		logDebug("Querying SKU details.");
		ArrayList<String> skuList = new ArrayList<String>();
		skuList.addAll(inv.getAllOwnedSkus());
		if (moreSkus != null)
			skuList.addAll(moreSkus);
		if (skuList.size() == 0) {
			logDebug("queryPrices: nothing to do because there are no SKUs.");
			return BILLING_RESPONSE_RESULT_OK;
		}
		Bundle querySkus = new Bundle();
		querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
		Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), ITEM_TYPE_INAPP, querySkus);
		if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
			int response = getResponseCodeFromBundle(skuDetails);
			if (response != BILLING_RESPONSE_RESULT_OK) {
				logDebug("getSkuDetails() failed: " + getResponseDesc(response));
				return response;
			} else {
				logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
				return IABHELPER_BAD_RESPONSE;
			}
		}
		ArrayList<String> responseList = skuDetails.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST);
		for (String thisResponse : responseList) {
			SkuDetails d = new SkuDetails(thisResponse);
			logDebug("Got sku details: " + d);
			inv.addSkuDetails(d);
		}
		return BILLING_RESPONSE_RESULT_OK;
	}

	void consumeAsyncInternal(final List<Purchase> purchases, final OnConsumeFinishedListener singleListener, final OnConsumeMultiFinishedListener multiListener) {
		final Handler handler = new Handler();
		flagStartAsync("consume");
		(new Thread(new Runnable() {
			public void run() {
				final List<IabResult> results = new ArrayList<IabResult>();
				for (Purchase purchase : purchases) {
					try {
						consume(purchase);
						results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
					} catch (IabException ex) {
						results.add(ex.getResult());
					}
				}
				flagEndAsync();
				if (singleListener != null) {
					handler.post(new Runnable() {
						public void run() {
							singleListener.onConsumeFinished(purchases.get(0), results.get(0));
						}
					});
				}
				if (multiListener != null) {
					handler.post(new Runnable() {
						public void run() {
							multiListener.onConsumeMultiFinished(purchases, results);
						}
					});
				}
			}
		})).start();
	}

	void logDebug(String msg) {
		if (mDebugLog)
			Log.d(mDebugTag, msg);
	}

	void logError(String msg) {
		Log.e(mDebugTag, "In-app billing error: " + msg);
	}

	void logWarn(String msg) {
		Log.w(mDebugTag, "In-app billing warning: " + msg);
	}
}
